// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdint.h>

#include <map>
#include <string>
#include <utility>
#include <vector>

#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/location.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "components/services/storage/public/mojom/quota_client.mojom.h"
#include "net/base/completion_once_callback.h"
#include "net/base/net_errors.h"
#include "storage/browser/database/database_quota_client.h"
#include "storage/browser/database/database_tracker.h"
#include "storage/browser/database/database_util.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/browser/quota/special_storage_policy.h"
#include "storage/common/database/database_identifier.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/quota/quota_types.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace storage {

// Declared to shorten the line lengths.
static const blink::mojom::StorageType kTemp =
    blink::mojom::StorageType::kTemporary;

// Mocks DatabaseTracker methods used by DatabaseQuotaClient.
class MockDatabaseTracker : public DatabaseTracker {
 public:
  MockDatabaseTracker()
      : DatabaseTracker(base::FilePath(),
                        /*is_incognito=*/false,
                        /*special_storage_policy=*/nullptr,
                        /*quota_manager_proxy=*/nullptr,
                        DatabaseTracker::CreatePassKey()) {}

  bool GetOriginInfo(const std::string& origin_identifier,
                     OriginInfo* info) override {
    auto found =
        mock_origin_infos_.find(GetOriginFromIdentifier(origin_identifier));
    if (found == mock_origin_infos_.end())
      return false;
    *info = OriginInfo(found->second);
    return true;
  }

  bool GetAllOriginIdentifiers(
      std::vector<std::string>* origins_identifiers) override {
    for (const auto& origin_info : mock_origin_infos_)
      origins_identifiers->push_back(origin_info.second.GetOriginIdentifier());
    return true;
  }

  bool GetAllOriginsInfo(std::vector<OriginInfo>* origins_info) override {
    for (const auto& origin_info : mock_origin_infos_)
      origins_info->push_back(OriginInfo(origin_info.second));
    return true;
  }

  void DeleteDataForOrigin(const url::Origin& origin,
                           net::CompletionOnceCallback callback) override {
    ++delete_called_count_;
    if (async_delete()) {
      base::SequencedTaskRunnerHandle::Get()->PostTask(
          FROM_HERE,
          base::BindOnce(&MockDatabaseTracker::AsyncDeleteDataForOrigin, this,
                         std::move(callback)));
      return;
    }
    std::move(callback).Run(net::OK);
  }

  void AsyncDeleteDataForOrigin(net::CompletionOnceCallback callback) {
    std::move(callback).Run(net::OK);
  }

  void AddMockDatabase(const url::Origin& origin, const char* name, int size) {
    MockStorageKeyInfo& info = mock_origin_infos_[origin];
    info.set_origin(GetIdentifierFromOrigin(origin));
    info.AddMockDatabase(base::ASCIIToUTF16(name), size);
  }

  int delete_called_count() { return delete_called_count_; }
  bool async_delete() { return async_delete_; }
  void set_async_delete(bool async) { async_delete_ = async; }

 protected:
  ~MockDatabaseTracker() override = default;

 private:
  class MockStorageKeyInfo : public OriginInfo {
   public:
    void set_origin(const std::string& origin_identifier) {
      origin_identifier_ = origin_identifier;
    }

    void AddMockDatabase(const std::u16string& name, int size) {
      EXPECT_FALSE(base::Contains(database_sizes_, name));
      database_sizes_[name] = size;
      total_size_ += size;
    }
  };

  int delete_called_count_ = 0;
  bool async_delete_ = false;
  std::map<url::Origin, MockStorageKeyInfo> mock_origin_infos_;
};

// Base class for our test fixtures.
class DatabaseQuotaClientTest : public testing::Test {
 public:
  const blink::StorageKey kStorageKeyA;
  const blink::StorageKey kStorageKeyB;
  const blink::StorageKey kStorageKeyOther;

  DatabaseQuotaClientTest()
      : kStorageKeyA(
            blink::StorageKey::CreateFromStringForTesting("http://host")),
        kStorageKeyB(
            blink::StorageKey::CreateFromStringForTesting("http://host:8000")),
        kStorageKeyOther(
            blink::StorageKey::CreateFromStringForTesting("http://other")),
        mock_tracker_(base::MakeRefCounted<MockDatabaseTracker>()) {}

  void TearDown() override {
    base::RunLoop run_loop;
    mock_tracker_->task_runner()->PostTask(FROM_HERE,
                                           base::BindLambdaForTesting([&]() {
                                             mock_tracker_->Shutdown();
                                             run_loop.Quit();
                                           }));
    run_loop.Run();
  }

  static int64_t GetStorageKeyUsage(mojom::QuotaClient& client,
                                    const blink::StorageKey& storage_key,
                                    blink::mojom::StorageType type) {
    int result = -1;
    base::RunLoop loop;
    client.GetStorageKeyUsage(storage_key, type,
                              base::BindLambdaForTesting([&](int64_t usage) {
                                result = usage;
                                loop.Quit();
                              }));
    loop.Run();
    EXPECT_GT(result, -1);
    return result;
  }

  static std::vector<blink::StorageKey> GetStorageKeysForType(
      mojom::QuotaClient& client,
      blink::mojom::StorageType type) {
    std::vector<blink::StorageKey> result;
    base::RunLoop loop;
    client.GetStorageKeysForType(
        type, base::BindLambdaForTesting(
                  [&](const std::vector<blink::StorageKey>& storage_keys) {
                    result = storage_keys;
                    loop.Quit();
                  }));
    loop.Run();
    return result;
  }

  static std::vector<blink::StorageKey> GetStorageKeysForHost(
      mojom::QuotaClient& client,
      blink::mojom::StorageType type,
      const std::string& host) {
    std::vector<blink::StorageKey> result;
    base::RunLoop loop;
    client.GetStorageKeysForHost(
        type, host,
        base::BindLambdaForTesting(
            [&](const std::vector<blink::StorageKey>& storage_keys) {
              result = storage_keys;
              loop.Quit();
            }));
    loop.Run();
    return result;
  }

  static blink::mojom::QuotaStatusCode DeleteStorageKeyData(
      mojom::QuotaClient& client,
      blink::mojom::StorageType type,
      const blink::StorageKey& storage_key) {
    blink::mojom::QuotaStatusCode result =
        blink::mojom::QuotaStatusCode::kUnknown;
    base::RunLoop loop;
    client.DeleteStorageKeyData(
        storage_key, type,
        base::BindLambdaForTesting([&](blink::mojom::QuotaStatusCode code) {
          result = code;
          loop.Quit();
        }));
    loop.Run();
    return result;
  }

  base::test::TaskEnvironment task_environment_;
  scoped_refptr<MockDatabaseTracker> mock_tracker_;
  base::WeakPtrFactory<DatabaseQuotaClientTest> weak_factory_{this};
};

TEST_F(DatabaseQuotaClientTest, GetStorageKeyUsage) {
  DatabaseQuotaClient client(*mock_tracker_);

  EXPECT_EQ(0, GetStorageKeyUsage(client, kStorageKeyA, kTemp));

  mock_tracker_->AddMockDatabase(kStorageKeyA.origin(), "fooDB", 1000);
  EXPECT_EQ(1000, GetStorageKeyUsage(client, kStorageKeyA, kTemp));

  EXPECT_EQ(0, GetStorageKeyUsage(client, kStorageKeyB, kTemp));
}

TEST_F(DatabaseQuotaClientTest, GetStorageKeysForHost) {
  DatabaseQuotaClient client(*mock_tracker_);

  EXPECT_EQ(kStorageKeyA.origin().host(), kStorageKeyB.origin().host());
  EXPECT_NE(kStorageKeyA.origin().host(), kStorageKeyOther.origin().host());

  std::vector<blink::StorageKey> storage_keys =
      GetStorageKeysForHost(client, kTemp, kStorageKeyA.origin().host());
  EXPECT_TRUE(storage_keys.empty());

  mock_tracker_->AddMockDatabase(kStorageKeyA.origin(), "fooDB", 1000);
  storage_keys =
      GetStorageKeysForHost(client, kTemp, kStorageKeyA.origin().host());
  EXPECT_EQ(storage_keys.size(), 1ul);
  EXPECT_THAT(storage_keys, testing::Contains(kStorageKeyA));

  mock_tracker_->AddMockDatabase(kStorageKeyB.origin(), "barDB", 1000);
  storage_keys =
      GetStorageKeysForHost(client, kTemp, kStorageKeyA.origin().host());
  EXPECT_EQ(storage_keys.size(), 2ul);
  EXPECT_THAT(storage_keys, testing::Contains(kStorageKeyA));
  EXPECT_THAT(storage_keys, testing::Contains(kStorageKeyB));

  EXPECT_TRUE(
      GetStorageKeysForHost(client, kTemp, kStorageKeyOther.origin().host())
          .empty());
}

TEST_F(DatabaseQuotaClientTest, GetStorageKeysForType) {
  DatabaseQuotaClient client(*mock_tracker_);

  EXPECT_TRUE(GetStorageKeysForType(client, kTemp).empty());

  mock_tracker_->AddMockDatabase(kStorageKeyA.origin(), "fooDB", 1000);
  std::vector<blink::StorageKey> storage_keys =
      GetStorageKeysForType(client, kTemp);
  EXPECT_EQ(storage_keys.size(), 1ul);
  EXPECT_THAT(storage_keys, testing::Contains(kStorageKeyA));
}

TEST_F(DatabaseQuotaClientTest, DeleteStorageKeyData) {
  DatabaseQuotaClient client(*mock_tracker_);

  mock_tracker_->set_async_delete(false);
  EXPECT_EQ(blink::mojom::QuotaStatusCode::kOk,
            DeleteStorageKeyData(client, kTemp, kStorageKeyA));
  EXPECT_EQ(1, mock_tracker_->delete_called_count());

  mock_tracker_->set_async_delete(true);
  EXPECT_EQ(blink::mojom::QuotaStatusCode::kOk,
            DeleteStorageKeyData(client, kTemp, kStorageKeyA));
  EXPECT_EQ(2, mock_tracker_->delete_called_count());
}

}  // namespace storage
